﻿using System;
using System.Collections.Generic;
using System.ComponentModel.Composition;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Cofe.Core;
using Cofe.Core.Dynamic;
using Cofe.Core.EntryType;
using Cofe.Core.Filter;
using Cofe.Core.Implements;
using Cofe.Core.Interfaces;
using Cofe.Core.Property;
using Cofe.Core.Service;
using Cofe.Core.Utils;
using Cofe.Data.Utils;

namespace Cofe.Data
{
    public class EFDatabaseEntryFactory : IDatabaseEntryFactory
    {
        #region Constructor

        internal EFDatabaseEntryFactory(EFRepository repository)
        {
            Repository = repository;
        }

        #endregion

        #region Methods

        private async Task<List<PropertyPair>> getNonORMPropertiesAsync(IPropertyHost propertyHost, string parseName)
        {
            var props = await propertyHost.Behaviors.GetSupportedPropertyAsync(PropertyRetrivalOptions.Default, MatchMode.All,
                new DatabasePropertyFilter(DatabaseCacheOptions.CachablePropertyPairs));
            return new List<PropertyPair>(await propertyHost.Behaviors.GetPropertyAsync(props));
        }

        private async Task<List<CofeSystemInfo>> getImplementationsAsync(IPropertyHost propertyHost, string parseName)
        {
            var castableInterfaces =
               CofeServices.PropertyInterfaceManager.GetCastableInterfaces(propertyHost, false);

            List<CofeSystemInfo> retVal = new List<CofeSystemInfo>();
            if (castableInterfaces.Contains(typeof(IEntryLink)))
                castableInterfaces = new List<Type>() { typeof(IEntryLink) };
            if (castableInterfaces.Count() == 0)
            {
                castableInterfaces = CofeServices.PropertyInterfaceManager.GetCastableInterfaces(propertyHost, true);
                if (castableInterfaces.Count() == 0)
                {
                    CofeServices.Logger.Log(String.Format("EFDEF: Cannot add {0} because it does not have any implementation", parseName),
                        Category.Warn, Priority.Medium);
                    return retVal;
                }
            }

            foreach (var ci in castableInterfaces)
                retVal.Add((CofeSystemInfo)(await EntryConstructor.ConstructEntryAsync(propertyHost, ci)));

            return retVal;
        }


#if DEBUG
        private static List<string> _constructedParseName = new List<string>();
#endif

        public List<Tuple<IPropertyHost, DatabaseEntry>> FindOrCreateDatabaseEntry(CofeDbContext context, params IPropertyHost[] propertyHosts)
        {
            var retVal = (from ph in propertyHosts
                   select new Tuple<IPropertyHost, DatabaseEntry>(ph, findOrCreateDatabaseEntry(context, ph))).ToList();
            context.SaveChanges();

            //Update EntryIds to PropertyHost.
            //foreach (var pp in retVal)
            //    pp.Item1.PropertyCache.CacheProperty(CacheProperties.Id, pp.Item2.Id);

            return retVal;
        }


        public DatabaseEntry findOrCreateDatabaseEntry(CofeDbContext context, IPropertyHost propertyHost)
        {
            //Console.WriteLine("FindOrCreateDatabaseEntry");

            string parseName = propertyHost.Behaviors.GetProperty<string>(CofeProperties.FullParseName);
            DatabaseEntry dbEntry = context.LocalOrDatabase<DatabaseEntry>(
                de => de.ParseName == parseName).FirstOrDefault();

            if (dbEntry != null)
            {
                
                //context.Entry(dbEntry).Property(de => de.LastCacheTime).CurrentValue = CofeConstants.MinDateTime;
            }
            else
            {
                string parentParseName = PathFE.GetDirectoryName(parseName);
                DatabaseEntry parentDbEntry = null;

                if (!(String.IsNullOrEmpty(parentParseName)))
                {
                    parentDbEntry = context.LocalOrDatabase<DatabaseEntry>(
                         de => de.ParseName == parentParseName).FirstOrDefault();

                    if (parentDbEntry == null)
                        parentDbEntry = (FindOrCreateDatabaseEntry(context,
                            propertyHost.Behaviors.GetProperty<ICofeDirectoryInfo>(CofeProperties.Parent).Properties)).First().Item2;
                }

                //DatabaseEntry linkedDbEntry = null;
                //if (propertyHost.Behaviors.IsPropertySupported(CofeEntryLinkProperties.LinkedEntryPH))
                //{
                //    var linkedPHEntry = propertyHost.Behaviors.GetProperty<IPropertyHost>(CofeEntryLinkProperties.LinkedEntryPH);
                //    linkedDbEntry = findOrCreateDatabaseEntry(context, linkedPHEntry);
                //}

                dbEntry = new DatabaseEntry()
                {
                    ParseName = parseName,
                    Label = propertyHost.Behaviors.GetProperty<string>(CofeProperties.Label),
                    SupportedProperties = "",
                    SupportDefinitions = "",
                    LastCacheTime = CofeConstants.MinDateTime,
                    LastListTime = CofeConstants.MinDateTime,
                };

                //if (linkedDbEntry != null)
                //    dbEntry.LinkedEntry = linkedDbEntry;

                if (parentDbEntry != null)
                    dbEntry.Parent = parentDbEntry;

                context.Entry(dbEntry).State = System.Data.EntityState.Added;
            }
           
            return dbEntry;
        }

        private async Task updatePropertiesAsync(CofeDbContext context, DatabaseEntry dbEntry, IPropertyHost propertyHost)
        {
            //Console.WriteLine("preupdatePropertiesAsync");

            var props = await getNonORMPropertiesAsync(propertyHost, dbEntry.ParseName);
            //Console.WriteLine(props);
            var collectionEntity = context.Entry(dbEntry).Collection(e => e.ExtendedProperties);

            if (!collectionEntity.IsLoaded)
                collectionEntity.Load();
            var tempList = collectionEntity.CurrentValue ?? new List<PropertyPair>();

            foreach (var prop in props)
            {
                var dbProp = tempList.FirstOrDefault(ce => ce.PropertyId == prop.PropertyId);
                if (dbProp == null)
                {
                    Console.WriteLine(
                        String.Format("Adding {0}'s {1} = {2}", dbEntry.ParseName,
                            prop.Property, prop.Value));
                    prop.EntityId = dbEntry.Id;
                    context.Entry(prop).State = System.Data.EntityState.Added;
                }
                else
                    if (!dbProp.Value.Equals(prop.Value))
                    {
                        Console.WriteLine(
                            String.Format("Updating {0}'s {1} from {2} to {3}", dbEntry.ParseName,
                                dbProp.Property, prop.Value, dbProp.Value));
                        dbProp.Value = prop.Value;
                        dbProp.ValueRetrieveTime = DateTime.UtcNow;

                        context.Entry(dbProp).State = System.Data.EntityState.Modified;
                    }
            }


            //Console.WriteLine("postupdatePropertiesAsync");
        }

        private async Task
            updateImplementationsAsync(CofeDbContext context, DatabaseEntry dbEntry, IPropertyHost propertyHost)
        {
            //Console.WriteLine("preupdateImplementationsAsync");
            var impls = await getImplementationsAsync(propertyHost, dbEntry.ParseName);
            var collectionEntity = context.Entry(dbEntry).Collection(e => e.Implementations);

            if (!collectionEntity.IsLoaded)
                collectionEntity.Load();
            var tempList = collectionEntity.CurrentValue.ToList();

            foreach (var imp in impls)
            {
                var dbImp = tempList.FirstOrDefault(e => e.Type == imp.Type);

                imp.Properties.PropertyCache.CacheProperty(CacheProperties.Id, dbEntry.Id);

                if (dbImp != null)
                {
                    Console.WriteLine(
                           String.Format("Updating {0}'s {1}", dbEntry.ParseName, imp.GetType()));
                    dbImp.Properties = imp.Properties;
                    context.Entry(dbImp).State = System.Data.EntityState.Modified;
                }
                else
                {
                    Console.WriteLine(
                           String.Format("Adding {0}'s {1}", dbEntry.ParseName, imp.GetType()));
                    context.Entry(imp).State = System.Data.EntityState.Added;
                }


            }
            //Console.WriteLine("postupdateImplementationsAsync");
        }

        public List<DatabaseEntry> CreateFullDatabaseEntry(CofeDbContext context, params IPropertyHost[] propertyHosts)
        {
            //Console.WriteLine("createFullDatabaseEntry");
            var dbEntryPairList = FindOrCreateDatabaseEntry(context, propertyHosts);
            foreach (var pp in dbEntryPairList)
            {
                var dbEntry = pp.Item2;
                var propertyHost = pp.Item1;
                var entityEntry = context.Entry(dbEntry);
                var lastWriteTime = propertyHost.Behaviors.GetProperty<DateTime>(CofeProperties.LastWriteTimeUtc);

                //Console.WriteLine(dbEntry.LastCacheTime);
                //Console.WriteLine(lastWriteTime);
                if (dbEntry.LastCacheTime < lastWriteTime || lastWriteTime == CofeConstants.MinDateTime)
                {
                    string origType = propertyHost.Behaviors.GetProperty<string>(CofeProperties.Type);
                    string supDef = PropertyUtils.PropertyDefinitionsToCsv(propertyHost.SupportedDefinitions);
                    string supProp = PropertyUtils.PropertiesToCsv(propertyHost.SupportedDefinitions.GetSupportedProperties());
                    string defEntryType = propertyHost.Behaviors.GetProperty<string>(CofeProperties.DefaultInterfaceTypeString);
                    int position = -1;
                    if (propertyHost.Behaviors.IsPropertySupported(CofeProperties.Position))
                        position = propertyHost.Behaviors.GetProperty<int>(CofeProperties.Position);

                    #region Check Linked Entry

                    DatabaseEntry dbLinkedEntry = null;
                    if (propertyHost.Behaviors.IsPropertySupported(CofeEntryLinkProperties.LinkedEntryPH))
                    {
                        var linkedEntryPH = propertyHost.Behaviors.GetProperty<IPropertyHost>(CofeEntryLinkProperties.LinkedEntryPH);
                        string linkedEntryParseName = linkedEntryPH.Behaviors.GetProperty<string>(CofeProperties.FullParseName);
                        if (linkedEntryParseName != dbEntry.ParseName)
                        {
                            dbLinkedEntry = context.LocalOrDatabase<DatabaseEntry>(de => de.ParseName == linkedEntryParseName).FirstOrDefault();
                            if (dbLinkedEntry == null)
                                dbLinkedEntry = CreateFullDatabaseEntry(context, linkedEntryPH).First();
                        }
                    }

                    #endregion
                    #region Check EntryType Info
                    EntryTypeInfo entryTypeInfo = propertyHost.Behaviors.IsPropertySupported(CofeEntryTypeProperties.EntryTypeInfo) ?
                       propertyHost.Behaviors.GetProperty<EntryTypeInfo>(CofeEntryTypeProperties.EntryTypeInfo) : null; ;

                    DatabaseEntry dbEntryTypeInfo = null;
                    if (entryTypeInfo != null)
                    {
                        dbEntryTypeInfo = context.LocalOrDatabase<DatabaseEntry>(de => de.ParseName == entryTypeInfo.ParseName).FirstOrDefault();
                        if (dbEntryTypeInfo == null)
                        {
                            dbEntryTypeInfo = CreateFullDatabaseEntry(context, entryTypeInfo.Properties).First(); ;
                        }
                    }


                    #endregion

                    entityEntry.Property(e => e.OriginalType).CurrentValue = origType;
                    //entityEntry.Property(e => e.Description).CurrentValue = origType;
                    entityEntry.Property(e => e.SupportDefinitions).CurrentValue = supDef;
                    entityEntry.Property(e => e.SupportedProperties).CurrentValue = supProp;
                    entityEntry.Property(e => e.DefaultEntryType).CurrentValue = defEntryType;
                    if (position != -1)
                        entityEntry.Property(e => e.Position).CurrentValue = position;
                    if (dbLinkedEntry != null)
                        entityEntry.Property(e => e.LinkedEntryId).CurrentValue = dbLinkedEntry.Id;
                    if (dbEntryTypeInfo != null)
                        entityEntry.Property(e => e.EntryTypeInfoId).CurrentValue = dbEntryTypeInfo.Id;

                    AsyncUtils.RunSync(() => updateImplementationsAsync(context, dbEntry, propertyHost));
                    AsyncUtils.RunSync(() => updatePropertiesAsync(context, dbEntry, propertyHost));
                    entityEntry.Property(e => e.LastCacheTime).CurrentValue = DateTime.UtcNow;
                }
            }

            //context.SaveChanges();
            return dbEntryPairList.Select(pp => pp.Item2).ToList();
        }

        private void updatePropertyHost(DatabaseEntry dbEntry, IPropertyHost ph)
        {
            if (ph.State != PropertyHostState.ORMtoCache)
                return; //Already updated.

            List<PropertyPair> cacheList = new List<PropertyPair>()
                {
                    PropertyPair.FromValue(CacheProperties.Id, dbEntry.Id),
                    PropertyPair.FromValue(CofeProperties.Position, dbEntry.Position),
                    PropertyPair.FromValue(CofeProperties.FullParseName, dbEntry.ParseName),
                    PropertyPair.FromValue(CofeProperties.Label, dbEntry.Label),
                    PropertyPair.FromValue(CofeProperties.Type, dbEntry.OriginalType),
                    PropertyPair.FromValue(CacheProperties.ParentId, dbEntry.ParentId),
                    PropertyPair.FromValue(CofeProperties.ParentParseName, PathFE.GetDirectoryName(dbEntry.ParseName)),
                };


            if (dbEntry.LinkedEntryId.HasValue)
            {
                using (var r = CofeDBServices.RepositoryFactory.CreateRepository())
                {
                    var linkPH = r.FindAll(FilterCollections.FromGuid(dbEntry.LinkedEntryId.Value)).FirstOrDefault();
                    if (linkPH == null)
                        throw new KeyNotFoundException("Link entry not found -" + dbEntry.LinkedEntryId.Value.ToString());                    
                    cacheList.Add(PropertyPair.FromValue(CofeEntryLinkProperties.LinkedEntryPH, linkPH));
                }                
            }


            cacheList.AddRange(dbEntry.ExtendedProperties);

            try
            {
                object[] props = PropertyUtils.CsvToProperties(dbEntry.SupportedProperties);
                IPropertyDefinitions[] propDefs = PropertyUtils.CsvToPropertyDefinitions(dbEntry.SupportDefinitions);

                cacheList.AddRange(
                    new[] {
                    PropertyPair.FromValue(CacheProperties.SupportedProperties, props),
                    PropertyPair.FromValue(CacheProperties.SupportedDefinitions, propDefs),
                    PropertyPair.FromValue(CofeProperties.DefaultInterfaceTypeString, dbEntry.DefaultEntryType)
                    });

                if (dbEntry.EntryTypeInfo != null && dbEntry.EntryTypeInfo.Implementations.Count > 0)
                {
                    var eti = dbEntry.EntryTypeInfo.Implementations.First();
                    updatePropertyHost(dbEntry.EntryTypeInfo, eti.Properties);
                    cacheList.Add(PropertyPair.FromValue(CofeEntryTypeProperties.EntryTypeInfo, eti));
                }

                //if (dbEntry.ParseName == @"{testDb1}\Image41.jpg" && dbEntry.Implementations.Count == 1)
                //    throw new Exception();
                //Console.WriteLine(dbEntry.ParseName + " - " + dbEntry.Implementations.Count().ToString());
                //foreach (var impl in dbEntry.Implementations)
                //    cacheList.AddRange(impl.Properties.PropertyCache.PropertyCacheDictionary.GetAllPropertyPairs());

                //Console.WriteLine(cacheList.Select(pp => pp.ToString()));

                AsyncUtils.RunSync(() => ph.PropertyCache.CachePropertyAsync(cacheList.ToArray()));
                ph.State = PropertyHostState.Default;
            }
            catch (Exception ex)
            {
                throw ex;
            }
        }

        private IPropertyCache createPropertyCache(DatabaseEntry dbEntry)
        {
            if (dbEntry.Implementations.Count() > 1)
            {
                var caches = (from i in dbEntry.Implementations select i.Properties.PropertyCache).ToArray();
                return CofeServices.ServiceLocater.FindService<IPropertyCacheFactory>().CreatePropertyCache(caches);
            }
            else return dbEntry.Implementations.First().Properties.PropertyCache;
        }

        public IPropertyHost UpdateImpl(DatabaseEntry dbEntry)
        {
            IPropertyHost retVal = dbEntry.Implementations.First().Properties;

            retVal.State = PropertyHostState.ORMtoCache;
            retVal.PropertyCache = createPropertyCache(dbEntry);
            updatePropertyHost(dbEntry, retVal);
            return retVal;
        }

        public T UpdateImpl<T>(DatabaseEntry dbEntry, T entry) where T : ICofeSystemInfo
        {
            if (entry == null)
                throw new ArgumentNullException("Entry");

            if (entry.Properties.State == PropertyHostState.ORMtoCache)
            {
                entry.Properties.PropertyCache = createPropertyCache(dbEntry);
                updatePropertyHost(dbEntry, entry.Properties);
            }

            return entry;
        }

        #endregion

        #region Data

        private IServiceLocater _serviceLocater = null;

        #endregion

        #region Public Properties

        public IServiceLocater ServiceLocater
        {
            get { return _serviceLocater ?? CofeServices.ServiceLocater; }
            set { _serviceLocater = value; }
        }

        public IDynamicPropertyInterfaceProvider[] InterfaceProviders
        {
            get { return ServiceLocater.FindAllServices<IDynamicPropertyInterfaceProvider>().ToArray(); }
        }

        private EFRepository Repository { get; set; }


        public IEntryConstructor EntryConstructor
        {
            get { return ServiceLocater.FindService<IEntryConstructor>(); }
        }

        #endregion



       
    }
}
